/*===========================================================================
  Copyright (C) 2013 by the Okapi Framework contributors
-----------------------------------------------------------------------------
  This library is free software; you can redistribute it and/or modify it 
  under the terms of the GNU Lesser General Public License as published by 
  the Free Software Foundation; either version 2.1 of the License, or (at 
  your option) any later version.

  This library is distributed in the hope that it will be useful, but 
  WITHOUT ANY WARRANTY; without even the implied warranty of 
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 
  General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License 
  along with this library; if not, write to the Free Software Foundation, 
  Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

  See also the full LGPL text here: http://www.gnu.org/copyleft/lesser.html
===========================================================================*/

package net.sf.okapi.lib.xliff2.document;

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Stack;

import net.sf.okapi.lib.xliff2.InvalidParameterException;
import net.sf.okapi.lib.xliff2.core.StartGroupData;
import net.sf.okapi.lib.xliff2.reader.Event;
import net.sf.okapi.lib.xliff2.reader.EventType;
import net.sf.okapi.lib.xliff2.reader.URIContext;

/**
 * Represents a group node.
 */
public class GroupNode extends GroupOrUnitNode {

	private GroupNode parent;
	private StartGroupData data;
	private LinkedHashMap<String, GroupOrUnitNode> nodes;

	/**
	 * Creates a new {@link GroupNode} object with a given {@link StartGroupData} resource and parent. 
	 * @param parent the parent of this new group node (use null for top-level groups).
	 * @param data the data for this new group node.
	 */
	public GroupNode (GroupNode parent,
		StartGroupData data)
	{
		if ( data == null ) {
			throw new InvalidParameterException("The data associated with the new group node must not be null.");
		}
		this.parent = parent;
		this.data = data;
		nodes = new LinkedHashMap<String, GroupOrUnitNode>();
	}
	
	/**
	 * Gets the {@link StartGroupData} resource for this group node.
	 * @return the resource for this group node.
	 */
	public StartGroupData get () {
		return data;
	}

	@Override
	public boolean isUnit () {
		return false;
	}

	/**
	 * Adds a {@link UnitNode} to this group node.
	 * @param node the unit node to add.
	 * @return the added unit node.
	 */
	public UnitNode add (UnitNode node) {
		nodes.put("u"+node.get().getId(), node);
		return node;
	}

	/**
	 * Adds a {@link GroupNode} to this group node.
	 * @param node the group node to add.
	 * @return the added group node.
	 */
	public GroupNode add (GroupNode node) {
		nodes.put("g"+node.get().getId(), node);
		return node;
	}
	
	/**
	 * Gets the parent for this group node.
	 * @return the parent for this group node, or null for a top-level group.
	 */
	public GroupNode getParent () {
		return parent;
	}
	
	/**
	 * Gets the {@link UnitNode} for a given unit id.
	 * The unit can be at any level in this group node.
	 * @param id the id to look for.
	 * @return the unit node for the given id, or null if none is found.
	 */
	public UnitNode getUnitNode (String id) {
		// Try at this level
		UnitNode item = (UnitNode)nodes.get("u"+id);
		if ( item != null ) return item;
		// Else: try recursively
		for ( GroupOrUnitNode node : nodes.values() ) {
			if ( !node.isUnit() ) {
				item = ((GroupNode)node).getUnitNode(id);
				if ( item != null ) return item;
			}
		}
		// Not found
		return null;
	}
	
	/**
	 * Gets a {@link GroupNode} for a given id.
	 * The group can be at any level in this group node.
	 * @param id the id to look for.
	 * @return the group node for the given id, or null if none is found.
	 */
	public GroupNode getGroupNode (String id) {
		// Try at this level
		GroupNode item = (GroupNode)nodes.get("g"+id);
		if ( item != null ) return item;
		// Else: try recursively
		for ( GroupOrUnitNode node : nodes.values() ) {
			if ( !node.isUnit() ) {
				item = ((GroupNode)node).getGroupNode(id);
				if ( item != null ) return item;
			}
		}
		// Not found
		return null;
	}
	
	/**
	 * Creates an iterator for the group nodes and unit nodes in this group node.
	 * @return a new iterator for the nodes in this group node.
	 */
	public Iterator<GroupOrUnitNode> createGroupOrUnitIterator () {
		return nodes.values().iterator();
	}

	/**
	 * Creates an iterator for the events in this group node.
	 * @param uriContext the URI context.
	 * @return a new iterator for the events in this group node.
	 */
	public Iterator<Event> createEventIterator (Stack<URIContext> uriContext) {
		EventIterator ei = new EventIterator () {
			
			private Iterator<GroupOrUnitNode> topIter = nodes.values().iterator();
			private Stack<Iterator<GroupOrUnitNode>> groups = new Stack<Iterator<GroupOrUnitNode>>();
			private Iterator<GroupOrUnitNode> groupIter = null;
			private int state = 0;
			
			@Override
			public boolean hasNext () {
				switch ( state ) {
				case 0:
					return true;
				case -1: // All done
					return false;
				}
				// State 1: normal items
				if ( groupIter != null ) {
					if ( groupIter.hasNext() ) return true;
					// Else: Done with this group: pop
					groups.pop();
					// Send an end-group event (for sub-group)
					state = 2;
					if ( groups.isEmpty() ) {
						groupIter = null;
					}
					else {
						groupIter = groups.peek();
					}
					return true;
				}
				// Else: top level items
				if ( topIter.hasNext() ) {
					return true;
				}
				// Done
				state = 3; // Send an end-group for this group
				return true;
			}

			@Override
			public Event next () {
				switch ( state ) {
				case 0: // Start of this group
					state = 1; // Next is normal items
					uriContext.push(uriContext.peek().clone());
					uriContext.peek().setGroupId(data.getId());
					return new Event(EventType.START_GROUP, uriContext.peek(), data);
				case 2: // End-group of sub-groups
					state = 1; // Next is normal items
					// The groupIter is already set to the previous group
					uriContext.pop();
					return new Event(EventType.END_GROUP, null);
				case 3: // End-group for this group
					state = -1;
					uriContext.pop();
					return new Event(EventType.END_GROUP, null);
				}
				
				// State 1: normal items
				if ( groupIter != null ) {
					GroupOrUnitNode node = groupIter.next();
					if ( node.isUnit() ) {
						uriContext.push(uriContext.peek().clone());
						uriContext.peek().setUnitId(((UnitNode)node).get().getId());
						Event event = new Event(EventType.TEXT_UNIT, uriContext.peek(), ((UnitNode)node).get());
						uriContext.pop();
						return event;
					}
					// Else: it's a group node
					GroupNode gn = (GroupNode)node;
					groups.push(gn.createGroupOrUnitIterator());
					groupIter = groups.peek();
					uriContext.push(uriContext.peek().clone());
					uriContext.peek().setGroupId(gn.get().getId());
					// Send the start-group of the sub-group
					return new Event(EventType.START_GROUP, uriContext.peek(), gn.get());
				}
				
				// Else: top nodes
				GroupOrUnitNode node = topIter.next();
				if ( node.isUnit() ) {
					uriContext.push(uriContext.peek().clone());
					uriContext.peek().setUnitId(((UnitNode)node).get().getId());
					Event event = new Event(EventType.TEXT_UNIT, uriContext.peek(), ((UnitNode)node).get());
					uriContext.pop();
					return event;
				}
				// Else: it's a group node
				GroupNode gn = (GroupNode)node;
				groups.push(gn.createGroupOrUnitIterator());
				groupIter = groups.peek();
				// Send the start-group of the sub-group
				uriContext.push(uriContext.peek().clone());
				uriContext.peek().setGroupId(gn.get().getId());
				return new Event(EventType.START_GROUP, uriContext.peek(), gn.get());
			}
		};
		ei.setURIContext(uriContext);
		return ei;
	}

}
